English
preview
Инжиниринг признаков для машинного обучения (Часть 1): дробное дифференцирование — стационарность без потери памяти

Инжиниринг признаков для машинного обучения (Часть 1): дробное дифференцирование — стационарность без потери памяти

MetaTrader 5Интеграция |
319 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Оглавление

  1. Введение
  2. Проблема: целочисленное дифференцирование уничтожает память
  3. Дилемма стационарности и памяти
  4. Математика дробного дифференцирования
  5. Генерация весов и сходимость
  6. Две стратегии окна: расширяющееся окно и окно фиксированной ширины (FFD)
  7. Проверка стационарности с помощью расширенного теста Дики — Фуллера
  8. Поиск оптимального d*
  9. Реализация в afml
  10. Интеграция в рабочий процесс
  11. Заключение
  12. Ссылки


Введение

В каждом процессе машинного обучения для финансовых временных рядов возникает решение по предварительной обработке, которое большинство практиков принимает почти автоматически: как преобразовать исходные цены в признаки. Стандартный подход — считать лог-доходности как первые разности логарифмов цен. Доходности стационарны, что соответствует предпосылкам большинства алгоритмов машинного обучения. Но у доходностей есть существенный недостаток: они стирают память. Ряд доходностей не содержит информации о ценовых уровнях, на которых находился актив, о степени его отклонения от долгосрочного среднего и о положении текущей цены относительно исторических зон поддержки и сопротивления. Каждое значение зависит ровно от одного предыдущего наблюдения, а вся более ранняя история отбрасывается.

Это не только теоретическая проблема. Равновесным моделям нужна память, чтобы оценивать, насколько ценовой процесс отклонился от ожидаемого значения. Стратегиям возврата к среднему нужна память, чтобы определить само среднее. Трендовым стратегиям нужна память, чтобы отличить устойчивое движение от шума. Когда целочисленное дифференцирование стирает эту память, алгоритму машинного обучения приходится восстанавливать ее через дополнительные признаки: лаговые доходности, скользящие статистики, технические индикаторы. Такие признаки остаются лишь приближенными заменителями информации, потерянной на этапе предварительной обработки.

Дробное дифференцирование решает эту дилемму. Вместо дифференцирования целочисленного порядка (0 для цен, 1 для доходностей) используется вещественный порядок d в диапазоне от 0 до 1. В результате получается стационарный ряд, который удовлетворяет предпосылкам моделей машинного обучения и при этом сохраняет как можно больше памяти исходного ценового ряда. Лопес де Прадо представил этот метод сообществу финансового машинного обучения в главе 5 AFML, опираясь на фундаментальную работу Хоскинга (1981). В статье рассматривается теория, объясняются две стратегии реализации — расширяющееся окно и окно фиксированной ширины — и разбирается промышленная реализация на Python в библиотеке afml. Следующая статья серии переносит этот механизм в MQL5 для работы с потоками данных MetaTrader 5 в реальном времени.


Проблема: целочисленное дифференцирование уничтожает память

Чтобы понять, какую проблему решает дробное дифференцирование, нужно увидеть, что именно уничтожает целочисленное дифференцирование. Рассмотрим ценовой ряд Pt. Любую операцию дифференцирования можно записать через оператор сдвига назад B, заданный как B^k X_t = X_{t−k}. Дифференцирование первого порядка имеет вид:

Оператор сдвига назад: первый порядок

Так получается привычный ряд доходностей. Операция использует вектор весов ω = {1, −1, 0, 0, …}: учитывается только одно предыдущее значение, а вся более ранняя история отбрасывается. Информация, содержащаяся в P_{t−2}, P_{t−3}, …, P_1, исчезает.

Дифференцирование нулевого порядка, то есть использование исходной цены, сохраняет всю память, но сами цены нестационарны. Их среднее и дисперсия меняются во времени. Алгоритм машинного обучения, обученный на признаках при уровне цены 100, не сможет корректно обобщать закономерности на данные с уровнем цены 200, поскольку изменилась связь между пространством признаков и пространством целевой переменной.

Визуально эффект хорошо заметен. Панель (a) ниже показывает синтетический ценовой ряд, сгенерированный геометрическим броуновским движением. Он нестационарен: ADF-тест не отвергает нулевую гипотезу о единичном корне. Панель (b) показывает лог-доходности (d = 1): они стационарны, но каждое значение лог-доходности уже не связано с уровнем цены, из которого оно было получено. Панель (c) показывает ряд, преобразованный методом FFD при d = 0.4: он стационарен, но общая форма и информация об уровне исходного ряда визуально сохраняются.

Исходные цены, доходности и ряд после FFD-преобразования

Рисунок 1. Три представления одного и того же базового процесса

  • Панель (a): исходные цены (d = 0), нестационарный ряд с полной памятью. Ряд свободно дрейфует: его среднее и дисперсия со временем смещаются, нарушая предпосылку стационарности для моделей машинного обучения.
  • Панель (b): лог-доходности (d = 1), стационарный ряд без памяти. Каждое значение лог-доходности зависит ровно от одного предыдущего наблюдения; вся информация об уровне цены стерта.
  • Панель (c): ряд FFD (d = 0.4), стационарный ряд с сохраненной памятью. Общая структура исходного ряда видна, а сам процесс колеблется вокруг стабильного среднего.


Дилемма стационарности и памяти

Ключевое противоречие просто: стационарность и память тянут в разные стороны. Стационарность — необходимое условие обучения с учителем: новые наблюдения нужно сопоставлять с размеченными обучающими примерами, а это работает только при стабильном распределении признаков. Но одной стационарности недостаточно для прогнозирования. Белый шум полностью стационарен, но не содержит сигнала. Прогнозная сила возникает из памяти — сохранения прошлой информации в текущих наблюдениях.

Рассмотрим актив, склонный к возврату к среднему. Его цена отражает, насколько она находится выше или ниже долгосрочного равновесия. Это расстояние до равновесия и есть сигнал, которым пользуется модель возврата к среднему. Когда мы вычисляем доходности, именно эта информация уничтожается. Доходность показывает, насколько цена изменилась между двумя соседними барами, но не показывает, где цена находится относительно равновесия.

Это не просто недочет при построении признаков, а фундаментальная потеря информации. Лопес де Прадо формулирует ее как дилемму между двумя традиционными эконометрическими парадигмами:

  1. Бокс — Дженкинс: работать с доходностями — стационарными, но лишенными памяти.
  2. Энгл — Грейнджер: работать с ценами через коинтеграцию; память сохраняется, но коинтегрирующие векторы нестабильны, а число коинтегрированных переменных ограничено.

Дробное дифференцирование открывает третий путь. Между крайностями d = 0 (цены) и d = 1 (доходности) существует непрерывный спектр преобразований, позволяющих гибко балансировать между стационарностью и памятью с помощью вещественного параметра d. Вопрос сводится к следующему: какое минимальное d обеспечивает стационарность? Это минимальное d* и есть оптимальная рабочая точка: любое дальнейшее дифференцирование удаляет память без необходимости.

Компромисс между стационарностью и памятью

Рисунок 2. Дилемма стационарности и памяти

  • Синяя кривая: сохраняемая память — монотонно уменьшается по мере роста d. При d = 0 (исходные цены) вся память сохранена. При d = 1 (доходности) память фактически равна нулю.
  • Красная кривая: стационарность — монотонно растет по мере увеличения d. При d = 0 ряд нестационарен. При d = 1 достигается полная стационарность.
  • Зеленая пунктирная кривая: условная прогнозная сила — достигает максимума около оптимального d* ≈ 0.35, где уже обеспечена стационарность и при этом сохранена максимальная память.


Математика дробного дифференцирования

Стандартное целочисленное дифференцирование использует биномиальное разложение (1 − B)^n, где n — положительное целое число. При n = 1: (1 − B)^1 = 1 − B, что дает X_t − X_{t−1}. При n = 2: (1 − B)^2 = 1 − 2B + B^2, что дает X_t − 2X_{t−1} + X_{t−2}. Во всех целочисленных случаях биномиальный коэффициент становится нулем после k > n членов, обрезая вектор весов и стирая память за лагом n.

Дробное дифференцирование обобщает показатель n до вещественного числа d с помощью биномиального ряда. Для любого вещественного d:

Оператор сдвига назад

где обобщенный биномиальный коэффициент равен:

Биномиальный ряд


Значение после дробного дифференцирования вычисляется как скалярное произведение вектора весов ω и исторических значений X:

Значение после дробного дифференцирования

Последовательность весов имеет вид ω = {1, −d, d(d−1)/2!, −d(d−1)(d−2)/3!, …}. Когда d — положительное целое число, произведение ∏(d−i) становится нулем при k > d, и все последующие веса исчезают — память обрывается. Когда d — положительное нецелое число (например, 0.4), произведение никогда не становится нулем, а веса образуют бесконечный ряд, асимптотически убывающий к нулю. Именно так дробное дифференцирование сохраняет память: старые наблюдения получают малые, но ненулевые веса.

Итеративное вычисление весов

Вычислять обобщенный биномиальный коэффициент с нуля для каждого k не нужно. Веса удовлетворяют рекуррентному соотношению, которое делает расчет эффективным. Начиная с ω0 = 1:

Вектор весов FFD

Одна формула итеративно генерирует всю последовательность весов. Каждый вес зависит только от предыдущего веса и текущего индекса, поэтому операция выполняется за постоянное время. На рисунке 3 показаны последовательности весов для d ∈ [0, 1] и d ∈ [1, 2].

Кривые убывания весов при разных d

Рисунок 3. Веса дробного дифференцирования ω_k при разных d

  • Панель (a): для d ∈ [0, 1] все веса после ω0 = 1 отрицательны и ограничены интервалом (−1, 0), убывая к нулю. При d = 0 ненулевым остается только ω0 = 1 (тождественное преобразование). При d = 1 получаем ω = {1, −1, 0, 0, …} — стандартные доходности.
  • Панель (b): при d > 1 выполняется ω1 < −1, а веса при k ≥ 2 становятся положительными — появляется поведение, характерное для второй разности.


Генерация весов и сходимость

Рекуррентное соотношение легко реализовать. В Python его удобно перенести в функцию, скомпилированную Numba для повышения скорости:

@njit(cache=True)
def get_weights(d, size):
    """Expanding window weights (AFML Section 5.4.2, page 79)."""
    weights = [1.0]
    for k in range(1, size):
        weights_ = -weights[-1] * (d - k + 1) / k
        weights.append(weights_)

    weights = np.array(weights[::-1]).reshape(-1, 1)
    return weights

Здесь важны два момента. Во-первых, перед возвратом веса разворачиваются: функция хранит их от самого старого к самому новому весу (ω0 = 1). Такой порядок соответствует соглашению, что prices[0] — самое старое значение, а prices[-1] — самое новое, поэтому скалярное произведение сразу возвращает значение после дробного дифференцирования. Во-вторых, декоратор @njit(cache=True) компилирует функцию в машинный код через Numba, что важно: при поиске гиперпараметра d функция может вызываться тысячи раз.

Для варианта окна фиксированной ширины добавляется критерий остановки по порогу. Веса асимптотически убывают к нулю; в какой-то момент вес становится настолько малым, что его включение почти не повышает точность, но увеличивает требуемую глубину ретроспективного окна. Генератор весов FFD останавливается, когда |ω_k| падает ниже порога τ (обычно 10^−5):

@njit(cache=True)
def get_weights_ffd(d, thres, lim):
    """Fixed-width window weights (AFML Section 5.4.2, page 83)."""
    weights = [1.0]
    k = 1
    ctr = 0
    while True:
        weights_ = -weights[-1] * (d - k + 1) / k
        if abs(weights_) < thres:
            break
        weights.append(weights_)
        k += 1
        ctr += 1
        if ctr == lim - 1:
            break

    weights = np.array(weights[::-1]).reshape(-1, 1)
    return weights

Количество сгенерированных весов — ширина окна — зависит и от d, и от τ. Меньшие значения d дают веса, которые убывают медленнее, поэтому для достижения порога нужны более длинные окна. На рисунке 4 показана эта зависимость.

Векторы весов FFD при разных значениях d

Рисунок 4. Векторы весов FFD при τ = 10^−5

  • Меньшие значения d: дают более длинные векторы весов, потому что веса убывают медленнее. При d = 0.2 окно охватывает сотни баров: каждое наблюдение FFD учитывает длительную историю.
  • Большие значения d: дают более короткие векторы. При d = 1.0 вектор равен {−1, 1} — это стандартные доходности с шириной окна 1.

Ширина окна и накопленная величина весов

Рисунок 5. Ширина окна FFD и сходимость весов

  • Панель (a): ширина окна как функция d. Меньшие значения d требуют пропорционально больше исторических данных для заполнения окна.
  • Панель (b): накопленная сумма модулей весов для d = 0.4. Около 99% общей величины веса приходится на первые ~30 лагов; оставшиеся веса дают все меньший вклад, но сохраняются ради точности.


Две стратегии окна: расширяющееся окно и окно фиксированной ширины (FFD)

Есть два способа применить вектор весов к конечному временному ряду. Выбор влияет и на статистические свойства результата, и на вычислительные затраты.

Расширяющееся окно

Подход с расширяющимся окном использует всю доступную историю для каждой точки. Для последнего наблюдения X̃_T используются веса {ω_k} при k = 0, …, T−1. Для X̃_{T−l} используются k = 0, …, T−l−1. Поэтому ранние наблюдения используют меньше весов, чем поздние, что создает асимметрию глубины истории.

Практическое последствие — отрицательный дрейф. По мере расширения окна растущее число отрицательных весов (напомним, что все веса после ω0 отрицательны при d ∈ (0,1)) добавляет новые отрицательные вклады. Ряд после дробного дифференцирования дрейфует вниз не потому, что базовая цена падает, а из-за артефакта расширяющегося окна.

Лопес де Прадо смягчает это, вводя порог потери веса τ. Для каждого наблюдения относительная потеря веса вычисляется как доля общей величины весов, которая находится за пределами доступного окна. Наблюдения, где эта потеря превышает τ, отбрасываются. Это улучшает ситуацию, но полностью не устраняет дрейф, потому что наблюдения все равно используют окна разной длины.

Окно фиксированной ширины (FFD)

Подход с окном фиксированной ширины (FFD) использует один и тот же вектор весов для каждого наблюдения. Последовательность весов усекается на первом индексе l*, где |ω_{l*+1}| < τ, и этот одинаковый усеченный вектор применяется ко всем X̃_t при t = l*, …, T. Первые l* наблюдений отбрасываются, потому что для заполнения окна еще недостаточно истории.

Этот подход полностью устраняет отрицательный дрейф. Каждое наблюдение вычисляется на одинаковой основе: одно и то же число весов и одна и та же глубина истории. Результат — бездрейфовое сочетание информации об уровне ряда и шумовой компоненты. Лопес де Прадо рекомендует FFD для практического применения; в реализации afml этот метод используется по умолчанию.

Компромисс связан с распределением: ряд FFD перестает быть нормально распределенным. Усечение вектора весов может вносить асимметрию и избыточный эксцесс. Для задач машинного обучения это допустимо: большинство деревьев решений и ансамблей на их основе не требуют конкретного распределения. Однако это важно для последующего анализа, если он предполагает нормальность.

Стратегии окна с расширяющейся историей и окна фиксированной ширины

Рисунок 6. Стратегии окон для дробного дифференцирования

  • Панель (a): расширяющееся окно. Вектор весов растет с каждым наблюдением; поздние бары учитывают больше истории, чем ранние, что приводит к отрицательному дрейфу из-за накопления отрицательных весов.
  • Панель (b): окно фиксированной ширины (FFD). Каждое наблюдение использует одну и ту же ширину окна, поэтому глубина истории остается постоянной по всему ряду. Первые несколько наблюдений отбрасываются, так как окно еще нельзя заполнить.


Проверка стационарности с помощью расширенного теста Дики — Фуллера

Дробное дифференцирование полезно только тогда, когда можно определить, стал ли результат стационарным. Расширенный тест Дики — Фуллера (ADF) — стандартный инструмент для такой проверки. Он проверяет нулевую гипотезу о том, что ряд содержит единичный корень, то есть является нестационарным, против альтернативы стационарности. Тест возвращает два важных значения: ADF-статистику (чем она более отрицательна, тем сильнее свидетельства в пользу стационарности) и p-значение (вероятность наблюдать такую статистику при нулевой гипотезе).

На уровне значимости 5% ряд считается стационарным, если (1) ADF-статистика ниже 5%-го критического значения и (2) p-значение ниже 0.05. Библиотека afml объединяет обе проверки в одной функции:

def adf_data(df1, df2, d=0, out_df=None, alpha=0.05):
    """ADF statistics and correlation between original and differenced series."""
    corr = np.corrcoef(df1.loc[df2.index], df2)[0, 1]
    adf = adfuller(df2, maxlag=1, regression="c", autolag=None)
    tc_col = f"{1 - alpha:.0%} conf"

    # Build results row
    columns = ["adfStat", "pVal", "lags", "nObs",
               "window", tc_col, "corr", "stationary"]
    vals = (list(adf[:4]) + [df1.shape[0] - adf[3]]
            + [adf[4][f"{alpha:.0%}"]] + [corr] + [False])

    if out_df is None or out_df.empty:
        out_df = pd.DataFrame.from_dict(
            {d: {k: v for k, v in zip(columns, vals)}}, orient="index"
        )
    else:
        out_df.loc[d, columns] = vals

    stationary = (out_df.loc[d, "adfStat"] < out_df.loc[d, tc_col]) and \
                 (out_df.loc[d, "pVal"] < alpha)
    out_df.loc[d, "stationary"] = stationary
    out_df.index.name = "d"
    return out_df

Функция также вычисляет корреляцию между исходным рядом и рядом после дробного дифференцирования. Эта корреляция измеряет сохранение памяти: чем ближе она к 1.0, тем больше структуры исходного ряда сохраняется после преобразования. На рисунке 7 показаны обе величины как функции d.

ADF-статистика и корреляция как функция d

Рисунок 7. ADF-статистика и корреляция как функция d

  • Синяя кривая (верхняя панель): корреляция между рядом FFD и исходными лог-ценами. При малых d она начинается около 1.0 и падает к нулю при d = 1, количественно показывая потерю памяти.
  • Зеленая кривая (нижняя панель): ADF-статистика. По мере роста d она становится все более отрицательной, то есть свидетельствует о большей стационарности. Красная пунктирная линия отмечает 95%-е критическое значение (−2.8623).
  • Точка пересечения: ADF-статистика пересекает критическое значение около d* ≈ 0.15, где корреляция с исходным рядом остается высокой.

Лопес де Прадо сообщает, что для 87 наиболее ликвидных фьючерсов мира стационарность достигалась при d < 0.6 во всех случаях, что подтверждает: стандартное целочисленное дифференцирование (d = 1) систематически чрезмерно дифференцирует данные.


Поиск оптимального d*

Поскольку ADF-статистика монотонно убывает по d (чем сильнее дифференцирование, тем более стационарным становится ряд), оптимальное d* — это наименьшее значение, при котором достигается порог стационарности. Это задача поиска корня, и естественный алгоритм для нее — бинарный поиск.

Поиск начинается с интервала [0, max_d], по умолчанию [0, 1]. На каждой итерации вычисляется ряд FFD в середине интервала, затем выполняется ADF-тест и интервал сужается: если середина дает стационарный ряд, верхняя граница сдвигается вниз (пробуем меньшее дифференцирование); если ряд нестационарен, нижняя граница сдвигается вверх (нужно более сильное дифференцирование). Сходимость до допуска tol требует ⌈log2(max_d / tol)⌉ итераций — около 10 итераций при tol = 10^−3.

Бинарный поиск оптимального d*

Рисунок 8. Бинарный поиск оптимального d*

  • Зеленые маркеры: значения d, при которых ADF-тест проходит, то есть ряд стационарен.
  • Красные маркеры: значения d, где ряд остается нестационарным.
  • Сходимость: интервал делится пополам на каждом шаге. К шагу 7 ширина интервала становится ниже порога допуска, и поиск завершается.

В реализации afml добавлены две оптимизации: (1) кэширование уже проверенных значений d и (2) проверка минимального размера выборки, по умолчанию 100 наблюдений. Если ряд FFD содержит меньше 100 непропущенных значений, верхняя граница поиска сдвигается вниз, к меньшим значениям d.

def fracdiff_optimal(
    series, fixed_width=True, alpha=0.05,
    max_d=1.0, tol=1e-3, use_log=True, verbose=False,
):
    """
    Binary search for minimum d that achieves stationarity.

    Returns (ffd_series, d_optimal, adf_dataframe).
    """
    low, high = 0.0, max_d
    best_d = None
    diff_adf = None
    out_df = None
    frac_diff_cache, adf_cache = {}, {}

    def frac_diff_cached(series, d, use_log):
        cache_key = (d, use_log)
        return frac_diff_cache.setdefault(
            cache_key,
            frac_diff_ffd(series, d, use_log=use_log)
            if fixed_width
            else frac_diff(series, d, use_log=use_log),
        )

    for i in range(20):  # max 20 iterations
        mid = (low + high) / 2
        diff = frac_diff_cached(series, mid, use_log)

        if len(diff) < 100:
            high = mid
            continue

        diff_adf = adf_data(series, diff, d=mid, out_df=diff_adf, alpha=alpha)

        if diff_adf.loc[mid, "stationary"]:
            best_series = diff.copy()
            best_d = mid
            high = mid
        else:
            low = mid

        if high - low < tol:
            break

    d = round(best_d, 4) if best_d is not None else max_d
    return best_series, d, diff_adf

Параметр use_log

Важная деталь: параметр use_log управляет тем, применяется ли логарифмирование перед дифференцированием. Для ценовых рядов его следует задавать как True: логарифмирование переводит мультипликативную динамику цен (простые доходности являются отношениями) в аддитивную динамику (лог-доходности являются разностями), поэтому дробные веса применяются в корректном пространстве. Для рядов, которые уже имеют аддитивную природу — доходностей, спредов, лог-цен, — задайте use_log=False, чтобы избежать двойного преобразования.


Реализация в afml

Промышленная реализация в afml.features.fracdiff использует внутренние циклы, скомпилированные Numba, для обеих стратегий окна. Внешняя логика обрабатывает индексы pandas, итерацию по столбцам DataFrame, пропуски NaN и прямое заполнение. Внутренний цикл — скалярное произведение весов и значений — выполняется как скомпилированный машинный код.

Расширяющееся окно: frac_diff

Функция расширяющегося окна вычисляет каждое наблюдение с использованием всей доступной истории до текущей точки. Ядро Numba выполняется параллельно по наблюдениям:

@njit(parallel=True, cache=True)
def _frac_diff_numba_core(series_values, weights, skip):
    """Numba-optimized core for expanding-window fractional differencing."""
    N = len(series_values)
    output_values = np.empty(N, dtype=np.float64)
    output_values[:] = np.nan

    for iloc in prange(skip, N):
        output_values[iloc] = np.dot(
            weights[-(iloc + 1):, :].T,
            series_values[:iloc + 1].reshape(-1, 1)
        )[0, 0]

    return output_values

Директива prange указывает Numba распределять итерации по ядрам процессора. Каждая итерация независима: скалярное произведение для наблюдения t не зависит от результата для наблюдения t−1, поэтому параллелизация безопасна. Параметр skip задает, сколько начальных наблюдений остается NaN из-за порога потери веса.

Окно фиксированной ширины: frac_diff_ffd

Функция FFD применяет один и тот же усеченный вектор весов к каждому наблюдению. Поскольку вектор весов постоянен, скалярное произведение для каждого наблюдения имеет фиксированный размер:

@njit(parallel=True, cache=True)
def _frac_diff_ffd_numba_core(series_values, weights, skip):
    """Numba-optimized core for fixed-width window fractional differencing."""
    N = len(series_values)
    weights = weights.T
    arr = np.empty(N, dtype=np.float64)

    for i in prange(skip, N):
        arr[i] = np.dot(
            weights, series_values[i - skip:i + 1]
        )[0, 0]

    return arr[skip:]

Эта функция проще и быстрее версии с расширяющимся окном. Вектор весов транспонируется один раз перед циклом, без повторной транспозиции внутри него, а срез series_values[i − skip : i + 1] всегда имеет одинаковую длину. При типичной ширине окна около 100 и ряде примерно из 10 000 баров вычисление занимает менее миллисекунды.

Внешняя обертка

Публичная функция frac_diff_ffd выполняет предварительную обработку — логарифмирование, распространение NaN, обработку входных данных типа Series и DataFrame — перед вызовом ядра Numba:

def frac_diff_ffd(series, d, thres=1e-5, use_log=True):
    """Fixed-width window fractional differentiation (AFML Section 5.5, page 83)."""
    if isinstance(series, pd.Series):
        series = series.copy().to_frame()

    if use_log:
        series_processed = np.log(series.clip(lower=1e-8))
    else:
        series_processed = series.copy()

    series_processed = series_processed.astype("float64")
    weights = get_weights_ffd(d, thres, series_processed.shape[0])
    width = len(weights) - 1

    df = {}
    for name in series_processed.columns:
        series_f = series_processed[[name]].ffill().dropna()
        ffd = _frac_diff_ffd_numba_core(series_f.values, weights, width)
        df[name] = pd.Series(ffd, index=series_f.index[width:])

    df = pd.concat(df, axis=1)
    if len(series_processed.columns) == 1:
        return df.squeeze()
    return df

clip(lower=1e-8) перед логарифмированием обеспечивает численную устойчивость: если какая-либо цена равна нулю или отрицательна, что возможно для спредов или синтетических рядов, логарифм не даст −∞. Цепочка ffill().dropna() сначала заполняет пропуски перед дифференцированием, а затем удаляет начальные строки NaN, гарантируя, что ядро Numba получает чистый непрерывный массив.

Ряд FFD, наложенный на исходные цены

Рисунок 9. Дробное дифференцирование фиксированной ширины, наложенное на цены

  • Зеленая кривая (левая ось): ряд FFD (d = 0.40) — стационарный, колеблется вокруг устойчивого среднего.
  • Синяя кривая (правая ось): исходный ценовой ряд. Тренды, смены режимов и относительные уровни явно отражаются в ряде FFD.
  • Корреляция ρ: количественно показывает, какая часть исходной структуры сохраняется после преобразования.


Интеграция в рабочий процесс

В рабочем процессе afml дробное дифференцирование относится к этапу построения признаков: оно выполняется после вычисления исходных признаков и перед передачей данных в модель. Типичный порядок действий:

  1. Вычислить исходные признаки по баровым данным: цену закрытия, VWAP, объем, микроструктурные признаки и т. д.
  2. Для каждого нестационарного признака (цены, накопленные объемы, накопительные суммы) применить fracdiff_optimal, чтобы найти минимальное d* и создать FFD-преобразованный признак.
  3. Проверить стационарность всей матрицы признаков с помощью is_stationary.
  4. Передать стационарные признаки с сохраненной памятью в процесс обучения модели.

Утилита is_stationary проходит по всем столбцам DataFrame и показывает, какие столбцы не удовлетворяют критерию стационарности по ADF-тесту:

def is_stationary(df: pd.DataFrame, alpha: float = 0.05, verbose: bool = True):
    not_stationary = []
    for col in df:
        adf = adfuller(df[col], maxlag=1, regression="c", autolag=None)
        if not (adf[0] < adf[4][f"{alpha:.0%}"] and adf[1] < alpha):
            not_stationary.append(col)
    return not_stationary

Рекомендуемый рабочий процесс Лопеса де Прадо

Практикам, начинающим с нуля, Лопес де Прадо рекомендует четырехшаговую процедуру, которая делает FFD применимым независимо от исходного вида признака:

  1. Вычислить накопленную сумму временного ряда. Это гарантирует, что некоторый порядок дифференцирования потребуется даже для исходно стационарного ряда: cumsum добавляет единичный корень.
  2. Вычислить FFD(d) для различных d ∈ [0, 1].
  3. Определить минимальное d, при котором p-значение ADF падает ниже 5%.
  4. Использовать ряд FFD(d) как прогнозный признак.

Эта процедура автоматизирована в fracdiff_optimal. Бинарный поиск заменяет ручную сетку на шаге 2, а функция возвращает и оптимальный ряд, и полную таблицу результатов ADF для проверки.

Особенности кэширования

Значение d* стабильно для заданного актива и таймфрейма: оно меняется только при существенном сдвиге структуры памяти ценового процесса, например при смене режима или структурном разрыве. В системе кэширования afml d* можно сохранять и повторно использовать между запусками без повторного бинарного поиска, если базовые данные существенно не изменились. Когда кэш хранит промежуточные ряды FFD, для fracdiff_optimal следует устанавливать auto_versioning=False, поскольку исходный код этой функции стабилен и не требует автоматической инвалидации кэша при каждом редактировании.


Заключение

Дробное дифференцирование решает противоречие между стационарностью и памятью, с которым сталкивается любой процесс финансового машинного обучения. Метод окна фиксированной ширины (FFD) — промышленный вариант этого подхода: он применяет постоянный, заранее вычисленный вектор весов ко всему ряду и создает стационарный результат без дрейфа, сохраняя максимум возможной памяти исходного ценового процесса. Бинарный поиск в fracdiff_optimal автоматизирует выбор минимального порядка дифференцирования d*, балансируя статистическую строгость (ADF-тест на выбранном уровне значимости) и сохранение памяти (корреляцию с исходным рядом).

Три свойства делают FFD особенно подходящим для промышленных рабочих процессов:

  1. Вектор весов фиксирован: его можно заранее вычислить один раз и многократно использовать для тех же d и τ.
  2. Каждое наблюдение зависит только от ограниченного ретроспективного окна, что делает метод совместимым с потоковыми архитектурами.
  3. Вычисление сводится к простому скалярному произведению O(l*) на бар; оно легко параллелизуется и эффективно выполняется на уровне аппаратуры.

Следующая статья использует эти свойства для реализации FFD-модуля реального времени в MQL5 для MetaTrader 5. Вектор весов хранится как статический массив в OnInit(). Ретроспективное окно получается через CopyClose() ровно для l*+1 баров. Скалярное произведение реализуется компактным циклом, который выполняется за микросекунды.


Ссылки

  1. López de Prado, М. (2018). Advances in Financial Machine Learning. Wiley. Глава 5: дробно-дифференцированные признаки.
  2. Hosking, J. R. M. (1981). «Fractional differencing». Biometrika, 68(1), 165–176.
  3. Jensen, A. N. and M. Ø. Nielsen (2014). «A fast fractional difference algorithm». Journal of Time Series Analysis, 35(5), 428–436.
  4. Hamilton, J. D. (1994). Time Series Analysis. Princeton University Press.
  5. Alexander, C. (2001). Market Models. Wiley. Глава 11.

Прикрепленные файлы

Файл Описание
fracdiff.py Полный модуль дробного дифференцирования: get_weights, get_weights_ffd, frac_diff, frac_diff_ffd, fracdiff_optimal, adf_data
stationary.py Утилита проверки стационарности: is_stationary

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22014

Прикрепленные файлы |
fracdiff.py (17.68 KB)
stationary.py (0.67 KB)
Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5 Архитектура машинного обучения для MetaTrader 5 (Часть 13): Реализация расчета размера позиции в MQL5
Мы создаем набор инструментов промышленного уровня для расчета размера позиции в MQL5: утилиты, фрагменты кода и пользовательские функции, которые повторяют исходные реализации на Python. Методы охватывают преобразование вероятности в размер позиции с коррекцией перекрытия, динамический расчет размера позиции по прогнозной цене (калиброванные сигмоидальная и степенная функции с лимитной ценой), бюджетирование на основе текущей занятости портфеля и резервный метод расчета размера позиции на основе модели смеси (EF3M). Результат — размер позиции со знаком в диапазоне [−1, ..., 1] плюс диагностика, которую можно напрямую подключить к логике ордеров.
Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения Разработка инструментария для анализа Price Action (Часть 48): Индекс гармонии нескольких таймфреймов с панелью взвешенного смещения
В этой статье представлен инструмент "Multi-Timeframe Harmony Index" – продвинутый советник для MetaTrader 5, который рассчитывает взвешенное смещение рынка по нескольким таймфреймам, сглаживает значения с помощью EMA и выводит результат на аккуратной панели на графике. Он поддерживает настраиваемые алерты и автоматически наносит сигналы покупки и продажи на график, когда значение смещения пересекает значимые пороги. Подходит трейдерам, которые используют анализ нескольких таймфреймов, чтобы соотносить точки входа с общей структурой рынка.
Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5 Разработка инструментария для анализа Price Action (Часть 49): Интеграция индикаторов тренда, моментума и волатильности в единую систему на MQL5
Упростите графики MetaTrader 5 с помощью советника Multi Indicator Handler. Этот интерактивный инструмент объединяет индикаторы тренда, моментума и волатильности в единую панель, работающую в реальном времени. Мгновенно переключайтесь между профилями, чтобы сосредоточиться на нужном вам типе анализа. Одним кликом скрывайте и показывайте элементы панели и сохраняйте фокус на движении цены. Читайте дальше, чтобы шаг за шагом узнать, как самостоятельно создать и настроить этот инструмент на MQL5.
Переосмысливаем классические стратегии (Часть 16): Стратегия пробоя двойных полос Боллинджера Переосмысливаем классические стратегии (Часть 16): Стратегия пробоя двойных полос Боллинджера
Эта статья знакомит читателя с переосмысленной версией классической стратегии пробоев полос Боллинджера. В ней определены ключевые недостатки первоначального подхода, такие как его хорошо известная подверженность ложным пробоям. Цель статьи - представить возможное решение: торговую стратегию двойных полос Боллинджера (Double Bollinger Band). Этот относительно малоизвестный подход устраняет слабые места классической версии и предлагает более динамичный взгляд на финансовые рынки. Он помогает преодолеть старые ограничения, определенные первоначальными правилами, предлагая трейдерам более устойчивую и адаптивную систему.